slate icon indicating copy to clipboard operation
slate copied to clipboard

Allow code to be organized in subfolders in `src/snippets` and `src/sections`

Open t-kelly opened this issue 6 years ago • 23 comments

Developers like to keep their repos organized. One way of doing this is through folders and file groupings. This organizational method is possible in folders like src/styles and src/scripts but not possible in src/snippets and src/sections.

Let's allow developers to create whatever folder structure they want in the main src/ folders, and then output those folders in the correct structure for Shopify servers in the dist/ folder.

For example

src/
-- snippets/
---- snippet-1/
------ snippet-1a.liquid
------ snippet-1b.liquid
---- snippet-2/
------ snippet-2a.liquid
------ snippet-2b.liquid

would be transformed into the following on build:

dist/
-- snippets/
---- snippet-1a.liquid
---- snippet-1b.liquid
---- snippet-2a.liquid
---- snippet-2b.liquid

Related https://github.com/Shopify/slate/issues/271

t-kelly avatar Sep 18 '17 19:09 t-kelly

I'd love to see this implemented for development. The only concern I have would be confusion if someone was using the same file name in more than one directory.

src/
-- snippets/
---- blog/
------ tags.liquid
---- collection/
------ tags.liquid

Would it make sense if the directory name were prepended to the file name as [directory].[filename].liquid? A quick test verified that {% include 'test.test' %} does correctly load /snippets/test.test.liquid

The example above would build as:

dist/
-- snippets/
------ blog.tags.liquid
------ collection.tags.liquid

jonathanmoore avatar Oct 11 '17 02:10 jonathanmoore

We are busily redesigning our entire site, and having this feature would be amazing. Our snippets and sections folders are starting to get very large. If we could break it off into folders the development experience will be a lot better. Please look into adding this feature.

presto2116 avatar Dec 19 '17 14:12 presto2116

@presto2116 This feature will be part of Slate v1. We are not adding any more features to Slate v0.

t-kelly avatar Dec 19 '17 18:12 t-kelly

Would it make sense if the directory name were prepended to the file name as [directory].[filename].liquid?

When I set up a directory structure, I expect to access it with slashes. So the most sensible way to consume a subfolder snippet would be {% include 'directory/filename' %}.

There's a conflict if you perform the proposed dot transform on a directory like this:

src/
-- snippets/
---- dogs/
------ fido.liquid
---- dogs.fido.liquid

Maybe as a prerequisite for this issue, Shopify's servers need to start accepting snippets subfolders.

NealEhardt avatar Apr 10 '18 21:04 NealEhardt

For anyone who comes across this in search of an answer to whether or not it's possible to include subdirectories in top level theme directories (e.g. snippets/ or sections/), I was able to get this working by altering my slate.config.js and extending the webpack config.

e.g.

const rules = [
  {
    test: /snippets\//,
    loader: 'file-loader',
    options: {
      name: `../snippets/[name].[ext]`,
    }
  },
];

module.exports = {
  slateTools: {
    webpackCommonExcludes: [
      'snippets/',
      'node_modules',
      'assets/static/',
    ],
    extends: {
      dev: { module: { rules } },
      prod:  { module: { rules } },
    },
  },
};

I haven't tested it out for directories other than src/snippets/, but I would hope it would work just the same. The major sticking point here for me was needing to include the webpackCommonExcludes option in order to have slate-tools core.js loaders ignore the theme directories with subdirectories in them and instead only use my extension to the webpack config. The thing that's not instantly apparent here is that initially I only included webpackCommonExcludes: ['snippets/'], which created a bunch of babel-loader issues for me. After discovering #705, I realized I needed to also add node_modules and assets/static/ to my ignore since they are the default values used and instead of augmented with my values, they're completely blown out.

I very briefly tested how webpack handles namespace collisions and it seemed like it was consistent with how it chose which file to place into dist/, although I did run into some wonky behavior when removing files but having them persist through. I have no idea exactly how it decides which file wins, but my major observation was that files more deeply nested won out over files in the same path as them e.g. snippets/group1/group2/test.liquid beat snippets/group1/test.liquid and snippets/test.liquid. Maybe someone has some insight into exactly how webpack is making this decision?

tshamz avatar Sep 12 '18 18:09 tshamz

@tshamz looks like you could make a PR? 😄

t-kelly avatar Sep 13 '18 00:09 t-kelly

const rules = [
  {
    test: /snippets\//,
    loader: 'file-loader',
    options: {
      name: `../snippets/[name].[ext]`,
    }
  },
];

module.exports = {
  slateTools: {
    webpackCommonExcludes: [
      'snippets/',
      'node_modules',
      'assets/static/',
    ],
    extends: {
      dev: { module: { rules } },
      prod:  { module: { rules } },
    },
  },
};

@tshamz Are you able to yarn deploy to production with that code? I'm trying and it's throwing errors:

Status: 422 Unprocessable Entity
Errors: Theme files may not be stored in subfolders

RustyDev avatar Oct 28 '18 22:10 RustyDev

yan, I'm able to deploy to the live published theme using yarn deploy, however I'm using the more recent version after breaking changes were introduced to slate.config.js (beta.8 or 9 I think?) so my config file looks a little different than yours now (don't mind all the other plugins and aliases):

/* eslint-disable */

// Configuration file for all things Slate.
// For more information, visit https://github.com/Shopify/slate/wiki/Slate-Configuration

const path = require('path');
const { ProvidePlugin } = require('webpack');

const plugins = [
  new ProvidePlugin({
    '$': 'jquery',
    'jQuery': 'jquery',
    'window.jQuery': 'jquery',
    'PubSub': 'pubsub-js',
  }),
];

const alias = {
  'jquery': path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es'),
  'slick': path.resolve('./node_modules/slick-carousel'),
  'styles': path.resolve('./src/assets/styles'),
  'scripts': path.resolve('./src/assets/scripts'),
  'core': path.resolve('./src/assets/scripts/core'),
  'bindings': path.resolve('./src/assets/scripts/bindings'),
  'containers': path.resolve('./src/assets/scripts/containers'),
  'handlers': path.resolve('./src/assets/scripts/handlers'),
  'nodes': path.resolve('./src/assets/scripts/nodes'),
  'controls': path.resolve('./src/assets/scripts/controls'),
};

const rules = [
  {
    test: require.resolve('jquery'),
    use: [{
      loader: 'expose-loader',
      options: '$'
    }]
  },
  {
    test: /snippets\/.*\.liquid$/,
    loader: 'file-loader',
    options: {
      name: `../snippets/[name].[ext]`,
    }
  },
  {
    test: /sections\/.*\.liquid$/,
    loader: 'file-loader',
    options: {
      name: `../sections/[name].[ext]`,
    }
  },
];

module.exports = {
  'cssVarLoader.liquidPath': [
    'src/snippets/base/css-variables.liquid'
  ],
  'webpack.extend': {
    plugins,
    resolve: { alias },
    module: { rules },
  },
  'webpack.commonExcludes': [
    /node_modules/,
    /assets\/static/,
    /sections\/.*\.liquid$/,
    /snippets\/.*\.liquid$/,
  ],
};

tshamz avatar Oct 30 '18 13:10 tshamz

Strange, I still get the same error after upgrading all packages and using your config (without plugins).

RustyDev avatar Oct 30 '18 22:10 RustyDev

Hi @tshamz , thank you for your configuration file. I'm trying to get this to work in Slate version 1.0.0-beta.12 so that I can have folders inside my snippets directory. I have installed the Webpack file-loader dependency & updated my Slate config, but Slate doesn't flatten the directory on output (just copies the subdirectories). Here's my config file:

/* eslint-disable */

// Configuration file for all things Slate.
// For more information, visit https://github.com/Shopify/slate/wiki/Slate-Configuration

const path = require('path');

const alias = {
  'jquery': path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es')
};

const rules = [
  {
    test: /snippets\/.*\.liquid$/,
    loader: 'file-loader',
    options: {
      name: `../snippets/[name].[ext]`,
    }
  },
  {
    test: /sections\/.*\.liquid$/,
    loader: 'file-loader',
    options: {
      name: `../sections/[name].[ext]`,
    }
  },
];

module.exports = {
  'cssVarLoader.liquidPath': [
    'src/snippets/css-variables.liquid'
  ],
  'webpack.extend': {
    resolve: {alias},
    module: {rules}
  },
  'webpack.commonExcludes': [
    /node_modules/,
    /assets\/static/,
    /sections\/.*\.liquid$/,
    /snippets\/.*\.liquid$/,
  ],
};

Would you mind letting me know what i'm doing wrong please? Thank you so much, Adam

adamwooding avatar Nov 16 '18 06:11 adamwooding

I created a new slate project using the config above to illustrate the issue. There's an app subdirectory under snippets with a file in it. After running yarn build, the subdirectory isn't flattened in dist/snippets:

https://github.com/RustyDev/slate-snippet-folder-test

https://github.com/RustyDev/slate-snippet-folder-test/blob/master/slate.config.js

https://github.com/RustyDev/slate-snippet-folder-test/tree/master/src/snippets/app

The only way I've been able to get this to work is by adding flatten: true in @shopify/slate-tools tools/webpack/config/parts/core.js:109:

{
  from: config.get('paths.theme.src.snippets'),
  to: config.get('paths.theme.dist.snippets'),
  flatten: true,
},

RustyDev avatar Nov 18 '18 19:11 RustyDev

hmmmmmm yah, something must have changed in one of the recent updates. Everything works for me running slate-tools v1.0.0-beta.9, but if I create a new slate project running slate-tools v1.0.0-beta.14 it appears as if my config no longer works.

tbh, webpack is voodoo magic to me. I don't have a very good grasp of what's going on behind the scenes. if you guys figure something out, report back in.

tshamz avatar Nov 27 '18 16:11 tshamz

@rustydev, I bet you could actually accomplish adding that flatten attribute via the slate config file instead of editing the module’s code, I’m just not sure how exactly. Maybe I’ll noodle on it for a bit and report back in.

tshamz avatar Nov 27 '18 23:11 tshamz

You should be able to add the following to your slate.config.js for this to work:

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new CopyWebpackPlugin(
      { from: 'src/snippets/*', to: 'dist/snippets', flatten: true },
      { from: 'src/sections/*', to: 'dist/sections', flatten: true }
    )
  ]
}

t-kelly avatar Nov 28 '18 13:11 t-kelly

I tried this (added the brackets since it's an array)

  plugins: [
    new CopyWebpackPlugin([
      { from: 'src/snippets/*', to: 'dist/snippets', flatten: true },
      { from: 'src/sections/*', to: 'dist/sections', flatten: true }
    ])
  ]

Doesn't seem to be working though:

Status: 422 Unprocessable Entity
Errors: Theme files may not be stored in subfolders

Here's my config:

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  'cssVarLoader.liquidPath': [
    'src/snippets/css-variables.liquid'
  ],
  'webpack.extend': {
    resolve: { 
      alias: {
        'jquery': path.resolve('./node_modules/jquery'),
        'lodash-es': path.resolve('./node_modules/lodash-es'),
      }
    },
    plugins: [
      new CopyWebpackPlugin([
        { from: 'src/snippets/*', to: 'dist/snippets', flatten: true },
        { from: 'src/sections/*', to: 'dist/sections', flatten: true }
      ])
    ]
  },
};

RustyDev avatar Nov 28 '18 18:11 RustyDev

@RustyDev try updating the paths to this...

new CopyWebpackPlugin([
    { from: 'snippets/**/*', to: '../snippets/', flatten: true },
    { from: 'sections/**/*', to: '../sections/', flatten: true },
]),

This will work, but anytime you update a file in one of your snippets/sections subdirectories, the watcher will upload two files. One of the uploads will succeed, and one will fail since the CopyWebpackPlugin from core.js is still trying to upload the unflattened file. Idk how to go about disabling the patterns for snippets/ and sections/ from the core.js instance of CopyWebpackPlugin, but if you can figure that out you should be good to go.

tshamz avatar Nov 28 '18 20:11 tshamz

Thanks, @tshamz. That was a silly error on my part. I'll give this a go.

I'm wondering at this point if it makes sense to open a pull request with that change to @shopify/slate-tools/tools/webpack/config/parts/core.js. Is there a reason that's not advisable? Seems like the easiest solution for this issue. After having everything organized in folders for a few weeks, it's made my codebase much easier to maintain. I'm sure others would feel the same.

RustyDev avatar Nov 29 '18 03:11 RustyDev

Idk if a pull request would get approved, but it’s worth a shot (especially since it’s such a tiny change codewise).

The reasons I think a PR might be met with some resistance are:

  1. the way CopyWebpackPlugin resolves namespace collisions is non-deterministic and for some people that’s not ideal

  2. based on what I can tell from digging into the slate-tools codebase and working with slate themes for a while now, it seems like everything has been architected so that developers can customize their slate projects via slate.config.js instead of needing to change the underlying codebase.

tshamz avatar Nov 29 '18 15:11 tshamz

after revisiting this briefly, I realized a quick and dirty workaround for this is to change the path for src/snippets/ in slate.config.js to either an empty directory or snippet subdirectory that you know won't have any further subdirectories and then adding an ignore property to the CopyWebpackPlugin added to the extended webpack config.

const CopyWebpackPlugin = require('copy-webpack-plugin')

const plugins = [
  new CopyWebpackPlugin([
      {
        from: 'snippets/**/*',
        to: '../snippets/',
        flatten: true,
        ignore: [ 'icons/*.liquid' ],
      },
  ]),
];

module.exports = {
  'cssVarLoader.liquidPath': ['src/snippets/css-variables.liquid'],
  'paths.theme.src.snippets': 'snippets/icons',
  'webpack.extend': {
    plugins,
  },
};

and src/snippets/ can look like: screen shot 2018-12-04 at 1 13 14 am without any errors or double uploading.

It's not very elegant or pretty, but it gets the job done and allows for subdirectories in src/snippets/ and src/sections/ again without any errors or odd behavior. If anyone figures out something cleaner, sound off.

tshamz avatar Dec 04 '18 09:12 tshamz

I was able to remove the subfolders after webpack build by adding this to my plugins. Requires copy-webpack-plugin on-build-webpack and rimraf

new CopyWebpackPlugin([
    {
        from: 'sections/**/*',
        to: '../sections/',
        flatten: true
    },
   {
        from: 'snippets/**/*',
        to: '../snippets/',
        flatten: true
    }
]),
new WebpackOnBuildPlugin(function(stats) {
    rimraf("./dist/sections/!(*.liquid)", function () { 
        console.log("removed section subfolders"); 
    });
     rimraf("./dist/snippets/!(*.liquid)", function () { 
         console.log("removed snippets subfolders"); 
     });
})

alexcasche avatar Jan 05 '19 17:01 alexcasche

^ I had the above working, but this approach seems to be broken after the introduction of the Slate Sections Plugin in https://github.com/Shopify/slate/pull/982/files#diff-59541ead0c8a4e18bc4cdcfda93131d5

ajhaupt7 avatar Apr 02 '19 19:04 ajhaupt7

@presto2116 This feature will be part of Slate v1. We are not adding any more features to Slate v0.

I see you already implemented "slate-sections-plugin" in v1. The problem described here is regarding snippets as well. Does this plugin work with snippets too? If not, can you please explain, why?

Thanks in advance, great app!

cjayyy avatar Jun 08 '19 17:06 cjayyy

Hi all, what is the state of play with this enhancement?

Working on our first slate theme and would be amazing if we could create subfolders for sections used in custom templates.

phpMagpie avatar Aug 12 '19 05:08 phpMagpie