Remove DataSource code from bundle.

This commit is contained in:
Dillon Kearns 2022-02-13 12:42:31 -08:00
parent b91f978d24
commit 88ca2f8a02
9 changed files with 697 additions and 17 deletions

View File

@ -47,7 +47,7 @@
"elm/virtual-dom": "1.0.2 <= v < 2.0.0",
"elm-community/dict-extra": "2.4.0 <= v < 3.0.0",
"elm-community/list-extra": "8.3.0 <= v < 9.0.0",
"jfmengels/elm-review": "2.5.0 <= v < 3.0.0",
"jfmengels/elm-review": "2.7.0 <= v < 3.0.0",
"jluckyiv/elm-utc-date-strings": "1.0.0 <= v < 2.0.0",
"justinmimbs/date": "4.0.0 <= v < 5.0.0",
"miniBill/elm-codec": "2.0.0 <= v < 3.0.0",

View File

@ -0,0 +1,34 @@
{
"type": "application",
"source-directories": [
"src",
"../../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/core": "1.0.5",
"elm-community/result-extra": "2.4.0",
"jfmengels/elm-review": "2.4.2",
"stil4m/elm-syntax": "7.2.5"
},
"indirect": {
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/parser": "1.1.0",
"elm/project-metadata-utils": "1.0.2",
"elm/random": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.3.0",
"elm-explorations/test": "1.2.2",
"miniBill/elm-unicode": "1.0.2",
"rtfeldman/elm-hex": "1.0.0",
"stil4m/structured-writer": "1.0.3"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -0,0 +1,9 @@
module ReviewConfig exposing (config)
import Pages.Review.DeadCodeEliminateData
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Pages.Review.DeadCodeEliminateData.rule ]

View File

@ -10,6 +10,7 @@ const terser = require("terser");
const os = require("os");
const { Worker, SHARE_ENV } = require("worker_threads");
const { ensureDirSync } = require("./file-helpers.js");
const { generateClientFolder } = require("./codegen.js");
const which = require("which");
let pool = [];
let pagesReady;
@ -174,9 +175,15 @@ async function runCli(options) {
}
async function compileElm(options) {
const outputPath = `dist/elm.js`;
const fullOutputPath = path.join(process.cwd(), `dist/elm.js`);
await spawnElmMake(options, ".elm-pages/Main.elm", outputPath);
ensureDirSync("dist");
const fullOutputPath = path.join(process.cwd(), `./dist/elm.js`);
await generateClientFolder(options.base);
await spawnElmMake(
options,
".elm-pages/Main.elm",
fullOutputPath,
path.join(process.cwd(), "./elm-stuff/elm-pages/client")
);
if (!options.debug) {
await runTerser(fullOutputPath);
@ -186,8 +193,7 @@ async function compileElm(options) {
function elmOptimizeLevel2(outputPath, cwd) {
return new Promise((resolve, reject) => {
const optimizedOutputPath = outputPath + ".opt";
const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath;
const fullOptimizedOutputPath = fullOutputPath + ".opt";
const fullOptimizedOutputPath = outputPath + ".opt";
const subprocess = spawnCallback(
`elm-optimize-level-2`,
[outputPath, "--output", optimizedOutputPath],
@ -210,7 +216,7 @@ function elmOptimizeLevel2(outputPath, cwd) {
commandOutput === "" &&
(await fs.fileExists(fullOptimizedOutputPath))
) {
await fs.copyFile(fullOptimizedOutputPath, fullOutputPath);
await fs.copyFile(fullOptimizedOutputPath, outputPath);
resolve();
} else {
if (!buildError) {
@ -235,11 +241,10 @@ async function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) {
if (!options.debug) {
await elmOptimizeLevel2(outputPath, cwd);
}
const fullOutputPath = path.join(cwd || process.cwd(), outputPath);
await fsPromises.writeFile(
fullOutputPath,
outputPath,
(
await fsPromises.readFile(fullOutputPath, "utf-8")
await fsPromises.readFile(outputPath, "utf-8")
).replace(
/return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_FORM_TO_STRING.\)/g,
"let appendSubmitter = (myFormData, event) => { event.submitter && event.submitter.name && event.submitter.name.length > 0 ? myFormData.append(event.submitter.name, event.submitter.value) : myFormData; return myFormData }; return " +
@ -270,9 +275,8 @@ function runElmMake(options, elmEntrypointPath, outputPath, cwd) {
cwd: cwd,
}
);
const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath;
if (await fs.fileExists(fullOutputPath)) {
await fsPromises.unlink(fullOutputPath, {
if (await fs.fileExists(outputPath)) {
await fsPromises.unlink(outputPath, {
force: true /* ignore errors if file doesn't exist */,
});
}
@ -281,11 +285,14 @@ function runElmMake(options, elmEntrypointPath, outputPath, cwd) {
subprocess.stderr.on("data", function (data) {
commandOutput += data;
});
subprocess.on("error", function () {
reject(commandOutput);
});
subprocess.on("close", async (code) => {
if (
code == 0 &&
(await fs.fileExists(fullOutputPath)) &&
(await fs.fileExists(outputPath)) &&
commandOutput === ""
) {
resolve();
@ -359,7 +366,7 @@ async function compileCliApp(options) {
await spawnElmMake(
options,
".elm-pages/Main.elm",
"elm.js",
path.join(process.cwd(), "elm.js"),
"./elm-stuff/elm-pages"
);

View File

@ -1,6 +1,9 @@
const fs = require("fs");
const fsExtra = require("fs-extra");
const copyModifiedElmJson = require("./rewrite-elm-json.js");
const copyModifiedElmJsonClient = require("./rewrite-client-elm-json.js");
const { elmPagesCliFile, elmPagesUiFile } = require("./elm-file-constants.js");
const spawnCallback = require("cross-spawn").spawn;
const {
generateTemplateModuleConnector,
} = require("./generate-template-module-connector.js");
@ -51,6 +54,92 @@ async function generate(basePath) {
]);
}
async function newCopyBoth(modulePath) {
await fs.promises.copyFile(
path.join(__dirname, modulePath),
path.join(`./elm-stuff/elm-pages/client/.elm-pages/`, modulePath)
);
}
async function generateClientFolder(basePath) {
console.log("@@@generateClientFolder", basePath);
const browserCode = generateTemplateModuleConnector(basePath, "browser");
const uiFileContent = elmPagesUiFile();
// console.log("@@@ generateClientFolder", process.cwd());
ensureDirSync("./elm-stuff/elm-pages/client/app");
ensureDirSync("./elm-stuff/elm-pages/client/.elm-pages");
await newCopyBoth("Page.elm");
await newCopyBoth("SharedTemplate.elm");
await newCopyBoth("SiteConfig.elm");
await copyModifiedElmJsonClient();
await fsExtra.copy("./app", "./elm-stuff/elm-pages/client/app", {
recursive: true,
});
await fs.promises.writeFile(
"./elm-stuff/elm-pages/client/.elm-pages/Main.elm",
browserCode.mainModule
);
await fs.promises.writeFile(
"./elm-stuff/elm-pages/client/.elm-pages/Route.elm",
browserCode.routesModule
);
await fs.promises.writeFile(
"./elm-stuff/elm-pages/client/.elm-pages/Pages.elm",
uiFileContent
);
await runElmReviewCodemod("./elm-stuff/elm-pages/client/");
}
/**
* @param {string} [ cwd ]
*/
async function runElmReviewCodemod(cwd) {
return new Promise((resolve, reject) => {
const child = spawnCallback(
`elm-review`,
[
"--fix-all",
"--report",
"json",
"--namespace",
"elm-pages",
"--config",
path.join(__dirname, "../../generator/dead-code-review"),
"--elmjson",
"elm.json",
],
{ cwd: path.join(process.cwd(), cwd || ".") }
);
let scriptOutput = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", function (/** @type {string} */ data) {
scriptOutput += data.toString();
});
child.stdin.write("y\n");
child.stderr.setEncoding("utf8");
child.stderr.on("data", function (/** @type {string} */ data) {
scriptOutput += data.toString();
});
child.on("error", function () {
reject(scriptOutput);
});
child.on("close", function (code) {
if (code === 0) {
resolve(scriptOutput);
} else {
reject(scriptOutput);
}
});
});
}
/**
* @param {string} moduleToCopy
* @returns { Promise<void> }
@ -113,4 +202,4 @@ function merge(arrays) {
return [].concat.apply([], arrays);
}
module.exports = { generate };
module.exports = { generate, generateClientFolder };

View File

@ -0,0 +1,49 @@
const fs = require("fs");
module.exports = async function () {
var elmJson = JSON.parse(
(await fs.promises.readFile("./elm.json")).toString()
);
// write new elm.json
await writeFileIfChanged(
"./elm-stuff/elm-pages/client/elm.json",
JSON.stringify(rewriteElmJson(elmJson))
);
};
function rewriteElmJson(elmJson) {
// The internal generated file will be at:
// ./elm-stuff/elm-pages/
// So, we need to take the existing elmJson and
// 1. remove existing path that looks at `Pages.elm`
elmJson["source-directories"] = elmJson["source-directories"].filter(
(item) => {
return item != ".elm-pages" && item != "app";
}
);
// 2. prepend ../../../ to remaining
elmJson["source-directories"] = elmJson["source-directories"].map((item) => {
return "../../../" + item;
});
// 3. add our own secret My.elm module 😈
elmJson["source-directories"].push(".elm-pages");
elmJson["source-directories"].push("app");
return elmJson;
}
async function writeFileIfChanged(filePath, content) {
if (
!(await fileExists(filePath)) ||
(await fs.promises.readFile(filePath, "utf8")) !== content
) {
await fs.promises.writeFile(filePath, content);
}
}
function fileExists(file) {
return fs.promises
.access(file, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

@ -20,7 +20,7 @@ function rewriteElmJson(elmJson) {
// 1. remove existing path that looks at `Pages.elm`
elmJson["source-directories"] = elmJson["source-directories"].filter(
(item) => {
return item != ".elm-pages" && item != "app";
return item != ".elm-pages";
}
);
// 2. prepend ../../../ to remaining

View File

@ -0,0 +1,155 @@
module Pages.Review.DeadCodeEliminateData exposing (rule)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Fix
import Review.Rule as Rule exposing (Direction, Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "Pages.Review.DeadCodeEliminateData"
{ moduleName = []
, isPageModule = False
}
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
declarationVisitor : Node Declaration -> context -> ( List (Error {}), context )
declarationVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration { documentation, declaration } ->
let
functionName : String
functionName =
Node.value declaration |> .name |> Node.value
in
case Node.value declaration of
{ name, expression } ->
case ( Node.value name, Node.value expression ) of
( "template", Expression.RecordExpr setters ) ->
let
dataFieldValue =
setters
|> List.filterMap
(\recordSetter ->
case Node.value recordSetter of
( keyNode, valueNode ) ->
if Node.value keyNode == "data" then
if isAlreadyApplied (Node.value valueNode) then
Nothing
else
recordSetter |> Just
else
Nothing
)
|> List.head
in
dataFieldValue
|> Maybe.map
(\dataValue ->
( [ Rule.errorWithFix
{ message = "Codemod"
, details = [ "" ]
}
(Node.range dataValue)
[ Review.Fix.replaceRangeBy (Node.range dataValue) "data = DataSource.fail \"\"\n "
]
]
, context
)
)
|> Maybe.withDefault
( [], context )
_ ->
( [], context )
_ ->
( [], context )
expressionVisitor : Node Expression -> context -> ( List (Error {}), context )
expressionVisitor node context =
case Node.value node of
Expression.Application applicationExpressions ->
case applicationExpressions |> List.map Node.value of
[ Expression.FunctionOrValue [ "Page" ] pageBuilderName, Expression.RecordExpr fields ] ->
let
dataFieldValue =
fields
|> List.filterMap
(\recordSetter ->
case Node.value recordSetter of
( keyNode, valueNode ) ->
if Node.value keyNode == "data" then
if isAlreadyApplied (Node.value valueNode) then
Nothing
else
recordSetter |> Just
else
Nothing
)
|> List.head
in
dataFieldValue
|> Maybe.map
(\dataValue ->
( [ Rule.errorWithFix
{ message = "Codemod"
, details = [ "" ]
}
(Node.range dataValue)
[ if pageBuilderName == "preRender" then
Review.Fix.replaceRangeBy (Node.range dataValue) "data = \\_ -> DataSource.fail \"\""
else
Review.Fix.replaceRangeBy (Node.range dataValue) "data = DataSource.fail \"\"\n "
]
]
, context
)
)
|> Maybe.withDefault
( [], context )
rest ->
( [], context )
_ ->
( [], context )
isAlreadyApplied : Expression -> Bool
isAlreadyApplied expression =
case expression of
Expression.LambdaExpression info ->
case Node.value info.expression of
Expression.Application applicationNodes ->
case applicationNodes |> List.map Node.value of
(Expression.FunctionOrValue [ "DataSource" ] "fail") :: rest ->
True
_ ->
False
_ ->
False
Expression.Application applicationNodes ->
case applicationNodes |> List.map Node.value of
(Expression.FunctionOrValue [ "DataSource" ] "fail") :: rest ->
True
_ ->
False
_ ->
False

View File

@ -0,0 +1,337 @@
module Pages.Review.DeadCodeEliminateDataTest exposing (all)
import Pages.Review.DeadCodeEliminateData exposing (rule)
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "Pages.Review.DeadCodeEliminateDataTest"
[ test "replaces data record setter" <|
\() ->
"""module Page.Index exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Page exposing (Page, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path
import Route exposing (Route)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{}
type alias Data =
()
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = data
}
|> Page.buildNoState { view = view }
data : DataSource Data
data =
DataSource.succeed ()
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Codemod"
, details =
[ "" ]
, under =
"""data = data
}"""
}
|> Review.Test.whenFixed
"""module Page.Index exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Page exposing (Page, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path
import Route exposing (Route)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{}
type alias Data =
()
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = DataSource.fail ""
}
|> Page.buildNoState { view = view }
data : DataSource Data
data =
DataSource.succeed ()
"""
]
, test "replaces data record setter with non-empty RouteParams" <|
\() ->
"""module Page.Blog.Slug_ exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Page exposing (Page, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path
import Route exposing (Route)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{ slug : String }
type alias Data =
()
page : Page RouteParams Data
page =
Page.preRender
{ data = data
, head = head
, pages = pages
}
|> Page.buildNoState { view = view }
data : DataSource Data
data =
DataSource.succeed ()
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Codemod"
, details =
[ "" ]
, under =
"""data = data"""
}
|> Review.Test.whenFixed
"""module Page.Blog.Slug_ exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Page exposing (Page, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path
import Route exposing (Route)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{ slug : String }
type alias Data =
()
page : Page RouteParams Data
page =
Page.preRender
{ data = \\_ -> DataSource.fail ""
, head = head
, pages = pages
}
|> Page.buildNoState { view = view }
data : DataSource Data
data =
DataSource.succeed ()
"""
]
, test "no fix after replacement is made" <|
\() ->
"""module Page.Index exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Page exposing (Page, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Path
import Route exposing (Route)
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
()
type alias RouteParams =
{}
type alias Data =
()
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = DataSource.fail ""
}
|> Page.buildNoState { view = view }
data : DataSource Data
data =
DataSource.succeed ()
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "replaces data record setter in Shared module" <|
\() ->
"""module Shared exposing (Data, Model, Msg, template)
import Browser.Navigation
import DataSource
import Html exposing (Html)
import Html.Styled
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import TableOfContents
import View exposing (View)
import View.Header
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
, view = view
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
}
type alias Data =
TableOfContents.TableOfContents TableOfContents.Data
type alias Model =
{ showMobileMenu : Bool
, counter : Int
, navigationKey : Maybe Browser.Navigation.Key
}
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Codemod"
, details =
[ "" ]
, under =
"""data = data
,"""
}
|> Review.Test.whenFixed
"""module Shared exposing (Data, Model, Msg, template)
import Browser.Navigation
import DataSource
import Html exposing (Html)
import Html.Styled
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import TableOfContents
import View exposing (View)
import View.Header
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
, view = view
, data = DataSource.fail ""
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
}
type alias Data =
TableOfContents.TableOfContents TableOfContents.Data
type alias Model =
{ showMobileMenu : Bool
, counter : Int
, navigationKey : Maybe Browser.Navigation.Key
}
"""
]
]